#!/usr/bin/python2.4
# jhebert@cs.washington.edu

import sys
import Queue
import threading

class ChartNode:
  """ 8/14/06
   This file holds the data for one computational chart.
   It could be at any level in the heirarchy.
   It handles emit calls in the computation stack and adds
   runnable jobs to the jobQueue zealously.
  """

  def __init__(self, threadPool, nodeName, log, runner=None):
    self.statusLog=log
    # The borderInputs are those which come from a chart above ths one.
    self.borderInputs = {}
    self.nodeName = nodeName
    # This maintains a list of which variables are waited on and
    # generated by which programs.
    self.sourceSinkTable = {}
    self.stateVars = {}
    # A queue of the emit calls for this node.
    self.emitQueue = Queue.Queue()
    # This is a mapping from the program names to the
    # chart/program objects that are themselves runnable.
    self.programTable = {}
    self.threadPool = threadPool
    self.runners = [runner]
    self.mutex = threading.RLock()

  def AddRunner(self, runner):
    """ Adding another chartRunner to this node. """
    message="Adding runner for node."
    self.statusLog.put((self.nodeName, message, runner.nodeName))
    self.mutex.acquire()
    self.runners.append(runner)
    self.mutex.release()
    self.UpdateInfo()

  def AddFollower(self, current, follower):
    """ Add a follower to the node current in this chart. """
    if((current==None)|(follower==None)):
      return
    toAdd=''.join([str(current),' -> ',str(follower)])
    self.statusLog.put((self.nodeName,'Adding follower',toAdd))
    self.mutex.acquire()
    if(current in self.sourceSinkTable):
      if(not(follower in self.sourceSinkTable[current])):
        self.sourceSinkTable[current].append(follower)
    else:
      self.sourceSinkTable[current] = [follower]
    self.mutex.release()
    self.UpdateInfo()

  def Emit(self, caller, varName, varValue):
    """ caller is a string of the module emitting a value.
    varName is the global name of the variable being emitted
    varValue is the value being emitted.
    Pass the value to the functions waiting on it and possibly
    emit it to the chart above this one. """
    toAdd=''.join([varName,' = ',str(varValue),' from:',str(caller)])
    self.statusLog.put((self.nodeName,'Received value',toAdd))
    self.emitQueue.put((caller, varName, varValue))
    self.threadPool.StartWorkerJob('chart', self)

  def IsListeningFor(self, varName):
    """ Is this chart listening for varName as an input. """
    toReturn = (varName in self.borderInputs)
    message='Am I listening for: '+varName
    self.statusLog.put((self.nodeName, message, str(toReturn)))
    return (varName in self.borderInputs)

  def AddNode(self, node, nodeName):
    """ Add the code object program under the programName. This
    allows it to be called and run once it adds a listener. """
    toAdd=''.join([nodeName,' = ',str(node)])
    self.statusLog.put((self.nodeName,'Adding node',toAdd))
    if(not (self in node.runners)):
      node.AddRunner(self)
    self.mutex.acquire()
    if(not nodeName in self.programTable):
      self.programTable[nodeName] = node
    if(not nodeName in self.sourceSinkTable):
      self.sourceSinkTable[nodeName] = []
    for var in node.borderInputs:
      self.borderInputs[var] = None
    self.mutex.release()
    self.UpdateInfo()

  def ReadyToRun(self):
    """ True if there are things for us to run. """
    toReturn = not(self.emitQueue.empty())
    message="Ready to run"
    self.statusLog.put((self.nodeName,message,str(toReturn)))
    return toReturn

  def __EmitHelper(self, node, varName, varValue):
    """ This recursively traces through the followers until the
    variable is accepted and used. It maintains a hash of which
    nodes are already searched so as to avoid cycles. """
    return self.__EmitHelperRecur(node, varName, varValue, {})

  def __EmitHelperRecur(self, node, varName, varValue, done):
    """ The recursive implementaion of __EmitHelper. """
    toAdd=''.join([varName,' = ',str(varValue)])
    self.statusLog.put((self.nodeName,'In emit helper for',toAdd))
    if(node in self.sourceSinkTable):
      followers = self.sourceSinkTable[node]
    else:
      followers=[]
    message='Checking for followers of: '+str(node)
    self.statusLog.put((self.nodeName,message,str(followers)))
    passedOn=False
    for f in followers:
      try:
        prog = self.programTable[f]
        if(prog in done):
          continue
        else:
          done[prog]=None
        if(prog.IsListeningFor(varName)):
          passedOn=True
          self.__Enqueue(f, varName, varValue)
        else:
          passedOn=True
          self.__EmitHelperRecur(f, varName, varValue, done)
      except RuntimeError, err:
        message = ("Unexpected error:%(excInfo)s"
                   "Error: %(err)s"
                   "self: %(self)s"
                   "node: %(node)s"
                   "varName: %(varName)s"
                   "varValue: %(varValue)s") % ({
          'excInfo' : str(sys.exc_info()[0]),
          'err' : str(err),
          'self': str(self),
          'node': str(node),
          'varName': str(varName),
          varValue: str(varValue), })
        for runner in self.runners:
          runner.Emit(self.nodeName, '_ERROR_', message)
    if(not passedOn):
      self.statusLog.put((self.nodeName,'Emitting to runner', toAdd))
      for runner in self.runners:
        runner.Emit(self.nodeName, varName, varValue)

  def __Enqueue(self, node, argName, argVal):
    """ Enqueue program 'node' to be run with argName and argVal.
    This recursively emits the value into the program/chart. """
    toAdd=''.join([argName,' = ',str(argVal),' for:',str(node)])
    self.statusLog.put((self.nodeName,'Enqueing variable',toAdd))
    prog = self.programTable[node]
    prog.Emit(self.nodeName, argName, argVal)
    if(prog.ReadyToRun()):
      self.threadPool.StartWorkerJob('prog', prog)

  def Run(self):
    """ Run the first item off of the emit queue. There
    might not be anything to run, but get to try. """
    self.statusLog.put((self.nodeName, 'Getting to run', self))
    self.__RunEmitQueue()

  def __RunEmitQueue(self):
    """ Emits are queued, this function processes them. """
    while(not(self.emitQueue.empty())):
      try:
        node, varName, varValue = self.emitQueue.get(False)
        passed = False
        toAdd=''.join([varName,' = ',str(varValue),' from:',str(node)])
        if(node in self.programTable):
          prog = self.programTable[node]
          if(varName in prog.stateVars):
            passed, message = True, "Enqueuing var"
            self.statusLog.put((self.nodeName, message, toAdd))
            self.__Enqueue(node, varName, varValue)
        if(not passed):
          self.statusLog.put((self.nodeName,'Emitting var',toAdd))
          self.__EmitHelper(node, varName, varValue)
      except Queue.Empty:
        pass

  def AssembleInfoPage(self):
    toReturn=(
      '<b>Name:</b> %(name)s\n'
      '<b>Inputs:</b> %(inputs)s\n'
      '<b>ChartRunners:</b> %(runners)s\n'
      '<b>ProgramTable:</b> %(programTable)s\n'
      '<b>SourceSinkTable:</b> %(sourceSink)s\n') % ({
      'name' : self.nodeName,
      'inputs' : ', '.join(self.borderInputs.keys()),
      'runners' : ', '.join([a.nodeName for a in self.runners]),
      'programTable' : ', '.join(self.programTable.keys()),
      'sourceSink' : self.FormatHash(self.sourceSinkTable), })
    return toReturn

  def FormatHash(self, hashTable):
    toReturn=[]
    for item in hashTable:
      toReturn.append(str(item)+' : '+str(hashTable[item]))
    return '\n'.join(toReturn)

  def UpdateInfo(self):
    data=self.AssembleInfoPage()
    self.statusLog.put((self.nodeName,'info',data))
